package assembler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import javax.swing.JOptionPane;


public class PassTwo {
	private static int start = 0;
	private static ListFile listFile;
	private static int lastLine = Assembler.sourceFile.length - 1;
	public static String textRecord = "";
	private static String objectCode = "";
	private static String label = "";
	private static int nextLocation = 0;
	private static int locationCounter = 0;
	private static int lineStart;
	private static int opCode;
	private final static int MAX_OUTPUT_LENGTH = 60;
	private static String SYMBOL_NOT_FOUND_ERROR = "Symbol not found";
	private static String IMMEDIATE_NUMBER_ERROR = "Immediate number out of range";
	private static boolean formatFour;
	private static boolean immediateAddressing;
	private static boolean indirectAddressing;
	private static String originalMnemonic;
	private static String originalOperand;
	private static String baseLabel = "";
	private static int baseAddress;

	public static void readLine(int index)	throws Exception{
		label = "";
		String[] line = Assembler.sourceFile[index];
		String[] nextLine = Assembler.sourceFile[index + 1];
		listFile = new ListFile();
		ArrayList<Comparable> finalList = new ArrayList<Comparable>();
		int lineNumber = Integer.parseInt(line[0]);
		if(line.length > 5 && line[4].equals("START")){
			nextLocation = Integer.parseInt(line[1], 16);
			listFile.setIntermediateLineValue(lineNumber, Integer.parseInt(line[1], 16), line[3], line[4], line[5]);
			Assembler.bufferedWriter.write(listFile.outputLine());
			Assembler.bufferedWriter.newLine();
			writeHeadRecord(line);
			return;
		}
		String mnemonic = null;
		String operand = null;
		String originalMnemonic = null;
		String originalOperand = null;		
		formatFour = false;
		immediateAddressing = false;
		indirectAddressing = false;		
		int byteLength = 0;
		int address = 0;		
		if (!isHexNumeric(line[1])){
			mnemonic = line[1];
			listFile.operand = "";
			if(line.length > 2){
				for (int i = 2; i < line.length; i++){
					listFile.operand += line[i] + " ";
				}	
			}
			listFile.setIntermediateLineValue(lineNumber, mnemonic, listFile.operand);
			if (mnemonic.equalsIgnoreCase("BASE")){
				Assembler.bufferedWriter.write(listFile.outputCommentLine(index));
				baseLabel = listFile.operand.trim();
			} else {
				Assembler.bufferedWriter.write(listFile.outputPassTwoCommentLine(index));
			}
			Assembler.bufferedWriter.newLine();
			return;
		} else {
			locationCounter = Integer.parseInt(line[1], 16);
			opCode = Integer.valueOf(line[2]);
		}
		originalMnemonic = "";
		switch(line.length){
		case 4:												
			mnemonic = testIfMnemonicIsFormatFour(line[line.length - 1]);
			operand = testForIndirectOrImmediateAddressing("");
			break;
		case 6:											
			label = line[line.length - 3];
			if (baseLabel.equals(label)){
				System.out.println(line[1]);
				baseAddress = Integer.parseInt(line[1], 16);
			}
			mnemonic = testIfMnemonicIsFormatFour(line[line.length - 2]);				
			operand = testForIndirectOrImmediateAddressing(line[line.length - 1]);
			break;
		default:
			mnemonic = testIfMnemonicIsFormatFour(line[line.length - 2]);
			operand = testForIndirectOrImmediateAddressing(line[line.length - 1]);
			break;
		}
		if(Assembler.isSICXE){
			finalList = SICXE(opCode, operand, mnemonic, line, address, lineNumber, byteLength);
		} else {
			finalList = SIC(opCode, operand, mnemonic, line, address, lineNumber, byteLength);
		}	
		//listFile.setIntermediateLineValue(lineNumber, locationCounter, label, originalMnemonic, operand, objectCode);
		listFile.setIntermediateLineValue((Integer)finalList.get(0), (Integer)finalList.get(1), finalList.get(2).toString(), finalList.get(3).toString(),
				finalList.get(4).toString(), finalList.get(5).toString());	
		Assembler.bufferedWriter.write(listFile.outputLine());
		Assembler.bufferedWriter.newLine();
		if(finalList.get(5).toString() != "")
			writeTextRecord(Integer.parseInt(line[1], 16), finalList.get(5).toString());	
	}

	public static void readLastLine() throws Exception{
		String[] line = Assembler.sourceFile[lastLine - 1];
		listFile = new ListFile();
		int lineNumber = Integer.parseInt(line[0]);		
		if(line.length == 3 && line[1].equals("END"))	{
			listFile.setIntermediateLineValue(lineNumber, line[1], line[2]);
		}
		Assembler.bufferedWriter.write(listFile.outputPassTwoLastLine());
		writeEndRecord();
	}

	private static void writeHeadRecord(String[] line) throws IOException{
		start = Integer.parseInt(line[1], 16);	
		int length = Integer.parseInt(Assembler.sourceFile[lastLine][0]);		
		Assembler.bufferedObjectCodeFileWriter.write(
				"H" + getSpaceString(line[3], 6) + integerToHexString(start, 6) + integerToHexString(length, 6));
		Assembler.bufferedObjectCodeFileWriter.newLine();
	}

	private static void writeTextRecord(int locationInteger, String objectCodeString) throws Exception	{
		int objectCodeLength = (int)(objectCodeString.length() * 0.5 + 0.5);		
		if(nextLocation == locationInteger)	{
			if(textRecord.isEmpty())
				lineStart = locationInteger;
			else if(textRecord.length() + objectCodeLength * 2 > MAX_OUTPUT_LENGTH)
				writeBuffer();
		}
		else{
			writeBuffer();
			lineStart = locationInteger;
		}
		textRecord += objectCodeString;							
		nextLocation = locationInteger + objectCodeLength;
	}

	public static void writeBuffer() throws Exception	{
		String lineOutput;
		if(textRecord.length() < MAX_OUTPUT_LENGTH)	{
			lineOutput = textRecord;
			textRecord = "";
		}
		else{
			lineOutput = textRecord.substring(0, MAX_OUTPUT_LENGTH);
			textRecord = textRecord.substring(MAX_OUTPUT_LENGTH);
		}
		Assembler.bufferedObjectCodeFileWriter.write(
				"T" + integerToHexString(lineStart, 6) + getBufferLengthString(lineOutput) + lineOutput);
		Assembler.bufferedObjectCodeFileWriter.newLine();		
		lineStart += (int)(lineOutput.length() * 0.5 + 0.5);
	}

	private static void writeEndRecord() throws Exception	{
		Assembler.bufferedObjectCodeFileWriter.write("E" + integerToHexString(start, 6));
	}

	public static String pad(String str, int length) {
		if(length < str.length())
			str = str.substring(str.length() - length, str.length());
		else {
			for(int i = 0; i < (length - str.length()); i++)
				str = "0" + str;
		}
		return str;
	}

	private static String integerToHexString(int value, int length)	{			
		return pad(Integer.toHexString(value).toUpperCase(), length);
	}

	private static String getSpaceString(String s, int length) {
		for(int i = 0; i < (length - s.length()); i++)	
			s += " ";
		return s;
	}

	private static String getBufferLengthString(String buf)	{
		return pad(Integer.toHexString((int) (buf.length() * 0.5 + 0.5)), 2).toUpperCase();
	}

	public static void outputSymbolTable() throws IOException{
		Assembler.bufferedWriter.newLine();
		Assembler.bufferedWriter.newLine();
		Assembler.bufferedWriter.write("Symbol Table");
		Assembler.bufferedWriter.newLine();
		Iterator<Entry<String, Integer>> symbolTableEntries = PassOne.symbolTable.entrySet().iterator();
		while (symbolTableEntries.hasNext()) {
			Map.Entry pairs = symbolTableEntries.next();
			Assembler.bufferedWriter.write(pairs.getKey().toString() + "\t" + pairs.getValue().toString());
			Assembler.bufferedWriter.newLine();
		}
	}

	private static void showErrorMessage(int line, String message) throws Exception{
		JOptionPane.showMessageDialog(null, "Error occured during Pass Two at line number "
				+ Integer.toString(line) + ": " + message, "Error!",
				JOptionPane.ERROR_MESSAGE);
		Assembler.bufferedWriter.close();
		System.exit(1);
	}

	private static String testIfMnemonicIsFormatFour (String mnemonicString){
		originalMnemonic = mnemonicString;
		if (mnemonicString.startsWith("+")){
			formatFour = true;
			return mnemonicString.substring(1);
		}
		return mnemonicString;
	}

	private static String testForIndirectOrImmediateAddressing (String operandString){
		originalOperand = operandString;
		if (operandString.startsWith("#")){
			immediateAddressing = true;
			return operandString.substring(1);
		}
		if (operandString.startsWith("@")){
			indirectAddressing = true;
			return operandString.substring(1);
		}		
		return operandString;
	}

	public static boolean isHexNumeric(String str)  
	{  
		try  
		{  
			int i = Integer.parseInt(str, 16);  
		}  
		catch(NumberFormatException nfe)  
		{  
			return false;  
		}  
		return true;  
	}

	private static ArrayList<Comparable> SIC(int opCode, String operand, String mnemonic, String[] line, int address,
			int lineNumber, int byteLength) throws Exception{
		ArrayList<Comparable> list = new ArrayList<Comparable>();
		if(opCode != OpCodeMap.WORD && opCode != OpCodeMap.BYTE 
				&& opCode != OpCodeMap.RESB && opCode != OpCodeMap.RESW && opCode != OpCodeMap.notFound){
			if(operand != "" && operand.contains(",X") ){
				address = (1 << 15);
				operand = line[line.length - 1].split(",X")[0];
			}
			if(PassOne.symbolTable.containsKey(operand))	{
				address += PassOne.symbolTable.get(operand);
			}else if(isHexNumeric(operand)){
				address = Integer.parseInt(operand);
			}else if(operand != ""){
				address = 0;
				showErrorMessage(lineNumber, SYMBOL_NOT_FOUND_ERROR);
			}
			objectCode = pad(Integer.toHexString(opCode)+Integer.toHexString(address), 6).toUpperCase();
			if(address == 0)
				objectCode = pad(Integer.toHexString(opCode).toUpperCase()+"0000", 6);
		}		
		else if(opCode == OpCodeMap.WORD){		
			objectCode = pad(integerToHexString(Integer.valueOf(line[5]), 6).toUpperCase(), 7);
		}
		else if(opCode == OpCodeMap.BYTE){
			String[] sta = line[5].split("'");			
			if(sta[0].equals("C")){
				int i = sta[1].length()-1;
				for(int bit = 0; i >= 0 ; i--, bit += 8)	{
					int tmp = sta[1].charAt(i);
					tmp <<= bit;
					address += tmp;
				}
				byteLength = sta[1].length() * 2;
				objectCode = integerToHexString(address, byteLength).toUpperCase();				
			}else if(sta[0].equals("X"))
				objectCode = sta[1];		
		}
		else
			objectCode = "";
		list.add(lineNumber);
		list.add(locationCounter);
		list.add(label);
		list.add(originalMnemonic);
		list.add(operand);
		list.add(objectCode);
		return list;
	}
	
	private static ArrayList<Comparable> SICXE(int opCode, String operand, String mnemonic, String[] line,
			int address, int lineNumber, int byteLength) throws Exception{
		ArrayList<Comparable> list = new ArrayList<Comparable>();
		objectCode = "";
		if(opCode != OpCodeMap.WORD && opCode != OpCodeMap.BYTE 
				&& opCode != OpCodeMap.RESB && opCode != OpCodeMap.RESW && opCode != OpCodeMap.notFound){
			if(operand != "" && OpCodeMap.isOpCodeInFormatTable(mnemonic)){	//Format 2
				int addressPartOne = 0x00;
				int addressPartTwo = 0x00;
				if(operand.contains(",")){
					String[] registerArray;
					registerArray = operand.split(",");
					addressPartOne = RegisterMap.getRegisterValue(registerArray[0]);
					addressPartTwo = RegisterMap.getRegisterValue(registerArray[1]);
				} else {
					addressPartOne = RegisterMap.getRegisterValue(operand);
					addressPartTwo = 0x00;
				}
				objectCode = (Integer.toHexString(opCode) + Integer.toHexString(addressPartOne)
						+ Integer.toHexString(addressPartTwo)).toUpperCase();
			}
			if(operand != "" && operand.contains(",X") && !OpCodeMap.isOpCodeInFormatTable(mnemonic)){
				address = (1 << 15);
				operand = line[line.length - 1].split(",X")[0];
			}
			if (immediateAddressing && isHexNumeric(operand)){
				address = Integer.parseInt(operand);
				if (0 <= address && address <= 4095) {
					objectCode = (pad(Integer.toHexString(opCode + 0x01), 2) 
							+ pad(Integer.toHexString(address), 6)).toUpperCase();
				} else if (4096 <= address && address <= 1048575 && formatFour){
					objectCode = (pad(Integer.toHexString(opCode + 1), 2) 
							+ "10"
							+ pad(Integer.toHexString(address), 4)).toUpperCase();
				} else {
					showErrorMessage(lineNumber, IMMEDIATE_NUMBER_ERROR);
				}
			}
			if (operand != "" && !isHexNumeric(operand)){
				if(PassOne.symbolTable.containsKey(operand)) {
					address += PassOne.symbolTable.get(operand);
					int targetAddressLessProgramCounter = (address - (locationCounter + 0x03));
					int targetAddressLessBase = (address - baseAddress);
				if (formatFour) {
						objectCode = (pad(Integer.toHexString(opCode + 0x03), 2) 
								+ "10"
								+ pad(Integer.toHexString(address), 4)).toUpperCase();
				} else if (-0x800 <= targetAddressLessProgramCounter &&
							targetAddressLessProgramCounter <= 0x7FF){
						String tempString = Integer.toHexString(targetAddressLessProgramCounter);
						String finalString = "";
						if (tempString.startsWith("f")){
							finalString = tempString.substring(tempString.length() - 3);
						} else {
							finalString = tempString;
						}
						objectCode = (pad(Integer.toHexString(opCode + 0x03), 2) 
								+ Integer.toHexString(0x02)
								+ pad(finalString, 3)).toUpperCase();
				} else {
					String tempString = Integer.toHexString(targetAddressLessBase);
					String twelveBitString = "";				
					if (tempString.equalsIgnoreCase("0") || tempString.equalsIgnoreCase("")){
						twelveBitString = "000";
					} else {
						twelveBitString = tempString.substring(1);
					}
					String flags = tempString.substring(0, 1);
					int flagInt = (Integer.parseInt(flags, 16) + 0x04);
					objectCode = (pad(Integer.toHexString(opCode + 0x03), 2)
							+ Integer.toHexString(flagInt)
							+ twelveBitString).toUpperCase();
				}
			}
			}
		}
		if(opCode == OpCodeMap.WORD){		
			objectCode = pad(integerToHexString(Integer.valueOf(line[5]), 6).toUpperCase(), 7);
		}
		else if(opCode == OpCodeMap.BYTE){
			String[] sta = line[5].split("'");			
			if(sta[0].equals("C")){
				int i = sta[1].length()-1;
				for(int bit = 0; i >= 0 ; i--, bit += 8)	{
					int tmp = sta[1].charAt(i);
					tmp <<= bit;
					address += tmp;
				}
				byteLength = sta[1].length() * 2;
				objectCode = integerToHexString(address, byteLength).toUpperCase();				
			}else if(sta[0].equals("X"))
				objectCode = sta[1];		
		}
		if(address == 0 && operand == "")
			objectCode = pad((Integer.toHexString(opCode + 0x03)).toUpperCase() + "0000", 6);
		list.add(lineNumber);
		list.add(locationCounter);
		list.add(label);
		list.add(originalMnemonic);
		list.add(originalOperand);
		list.add(objectCode);
		return list;
	}
}